package net.aiion.weave.impl.cards.transformation;

import java.util.ArrayList;
import java.util.Collection;

import net.aiion.weave.asm.Label;
import net.aiion.weave.asm.Opcodes;
import net.aiion.weave.asm.Type;
import net.aiion.weave.spi.bytecode.Bytecode;
import net.aiion.weave.spi.bytecode.BytecodeContext;
import net.aiion.weave.spi.bytecode.BytecodeException;
import net.aiion.weave.spi.bytecode.BytecodeGenerator;
import net.aiion.weave.spi.bytecode.events.AnnotationEndBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.AnnotationEnumValueBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.AnnotationStartBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.AnnotationValueBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.BytecodeEvent;
import net.aiion.weave.spi.bytecode.events.ClassEndBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.ClassStartBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.FieldEndBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.FieldStartBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.MethodEndBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.MethodStartBytecodeEvent;
import net.aiion.weave.spi.bytecode.util.BytecodeTransformation;
import net.aiion.weave.spi.cards.CardException;
import net.aiion.weave.spi.cards.CardProperties;
import net.aiion.weave.spi.cards.annotations.Access;
import net.aiion.weave.spi.cards.annotations.Property;

// TODO add support for inheritance
public class CardPropertiesTransformation extends BytecodeTransformation
{
    private String className;

    private String fieldName;
    private String fieldDescriptor;

    private String methodName;
    private String methodDescriptor;

    private String propertyName;
    private Access propertyAccess;

    private boolean hasPropertyAnnotation;
    private boolean inPropertyAnnotation;

    private Collection<BytecodeEvent> getPropertyBytecodeEvents;
    private BytecodeGenerator getPropertyBytecodeGenerator;

    private Collection<BytecodeEvent> setPropertyBytecodeEvents;
    private BytecodeGenerator setPropertyBytecodeGenerator;

    public CardPropertiesTransformation() {
    }

    @Override
    public boolean testRetain(final BytecodeContext aBytecodeContext, final BytecodeEvent aBytecodeEvent) {
        return aBytecodeEvent.getType() == BytecodeEvent.CLASS_START
            && !aBytecodeEvent.asClassStart().containsInterface(CardProperties.class);
    }

    @Override
    public boolean testProcess(final BytecodeContext aBytecodeContext, final BytecodeEvent aBytecodeEvent) {
        return aBytecodeEvent.getType() == BytecodeEvent.ANNOTATION_START
            && aBytecodeEvent.asAnnotationStart().equalsType(Property.class);
    }

    @Override
    public boolean testSkip(final BytecodeContext aBytecodeContext, final BytecodeEvent aBytecodeEvent) {
        return aBytecodeEvent.getType() == BytecodeEvent.CLASS_START
            && (aBytecodeEvent.asClassStart().isInterface() || aBytecodeEvent.asClassStart().containsInterface(
                CardProperties.class));
    }

    @Override
    public void handleBytecodeEvent(final BytecodeContext aBytecodeContext, final BytecodeEvent aBytecodeEvent) {
        switch (aBytecodeEvent.getType()) {
        case BytecodeEvent.CLASS_START:
            handleClassStartBytecodeEvent(aBytecodeContext, aBytecodeEvent.asClassStart());
            break;
        case BytecodeEvent.ANNOTATION_START:
            handleAnnotationStartBytecodeEvent(aBytecodeContext, aBytecodeEvent.asAnnotationStart());
            break;
        case BytecodeEvent.ANNOTATION_VALUE:
            handleAnnotationValueBytecodeEvent(aBytecodeContext, aBytecodeEvent.asAnnotationValue());
            break;
        case BytecodeEvent.ANNOTATION_ENUM_VALUE:
            handleAnnotationEnumValueBytecodeEvent(aBytecodeContext, aBytecodeEvent.asAnnotationEnumValue());
            break;
        case BytecodeEvent.ANNOTATION_END:
            handleAnnotationEndBytecodeEvent(aBytecodeContext, aBytecodeEvent.asAnnotationEnd());
            break;
        case BytecodeEvent.FIELD_START:
            handleFieldStartBytecodeEvent(aBytecodeContext, aBytecodeEvent.asFieldStart());
            break;
        case BytecodeEvent.FIELD_END:
            handleFieldEndBytecodeEvent(aBytecodeContext, aBytecodeEvent.asFieldEnd());
            break;
        case BytecodeEvent.METHOD_START:
            handleMethodStartBytecodeEvent(aBytecodeContext, aBytecodeEvent.asMethodStart());
            break;
        case BytecodeEvent.METHOD_END:
            handleMethodEndBytecodeEvent(aBytecodeContext, aBytecodeEvent.asMethodEnd());
            break;
        case BytecodeEvent.CLASS_END:
            handleClassEndBytecodeEvent(aBytecodeContext, aBytecodeEvent.asClassEnd());
            break;
        default:
            break;
        }

        aBytecodeContext.forwardBytecodeEvent(aBytecodeEvent);
    }

    private void handleClassStartBytecodeEvent(final BytecodeContext aBytecodeContext,
        final ClassStartBytecodeEvent aClassStartBytecodeEvent)
    {
        className = aClassStartBytecodeEvent.getName();
        aClassStartBytecodeEvent.addInterface(CardProperties.class);

        prepareGetProperty();
        prepareSetProperty();
    }

    private void handleAnnotationStartBytecodeEvent(final BytecodeContext aBytecodeContext,
        final AnnotationStartBytecodeEvent aAnnotationStartBytecodeEvent)
    {
        if (aAnnotationStartBytecodeEvent.equalsType(Property.class)) {
            inPropertyAnnotation = true;
            hasPropertyAnnotation = true;
        }
    }

    private void handleAnnotationValueBytecodeEvent(final BytecodeContext aBytecodeContext,
        final AnnotationValueBytecodeEvent aAnnotationValueBytecodeEvent)
    {
        if (inPropertyAnnotation && "name".equals(aAnnotationValueBytecodeEvent.getName())) {
            propertyName = (String) aAnnotationValueBytecodeEvent.getValue();
        }
    }

    private void handleAnnotationEnumValueBytecodeEvent(final BytecodeContext aBytecodeContext,
        final AnnotationEnumValueBytecodeEvent aAnnotationEnumValueBytecodeEvent)
    {
        if (inPropertyAnnotation && "value".equals(aAnnotationEnumValueBytecodeEvent.getName())) {
            propertyAccess = Access.valueOf(aAnnotationEnumValueBytecodeEvent.getValue());
        }
    }

    private void handleAnnotationEndBytecodeEvent(final BytecodeContext aBytecodeContext,
        final AnnotationEndBytecodeEvent aAnnotationEndBytecodeEvent)
    {
        if (inPropertyAnnotation) {
            testPublishedMethod();
            inPropertyAnnotation = false;
        }
    }

    public void testPublishedMethod() {
        if (methodName == null) {
            return;
        }

        switch (propertyAccess) {
        case READ_ONLY:
            if (!methodDescriptor.startsWith("()") || methodDescriptor.endsWith(")V")) {
                throw new BytecodeException(
                    String
                        .format(
                            "@Property annotated method '%s' in class '%s' with read-only access must accept no arguments and must not return void.",
                            methodName, className));
            }
            break;
        case WRITE_ONLY:
            if (methodDescriptor.startsWith("()") || !methodDescriptor.endsWith(")V")
                || Type.getArgumentTypes(methodDescriptor).length != 1)
            {
                throw new BytecodeException(
                    String
                        .format(
                            "@Property annotated method '%s' in class '%s' with write-only access must accept exactly one argument of any type and must return void.",
                            methodName, className));
            }
            break;
        case READ_WRITE:
            throw new BytecodeException(String.format(
                "@Property annotated method '%s' in class '%s' must either be read-only or write-only.", methodName,
                className));
        default:
            throw new BytecodeException("Unsupported property access type '" + propertyAccess + "'");
        }

    }

    private void handleFieldStartBytecodeEvent(final BytecodeContext aBytecodeContext,
        final FieldStartBytecodeEvent aFieldStartBytecodeEvent)
    {
        fieldName = aFieldStartBytecodeEvent.getName();
        fieldDescriptor = aFieldStartBytecodeEvent.getDescriptor();
        propertyName = fieldName;
    }

    private void handleFieldEndBytecodeEvent(final BytecodeContext aBytecodeContext,
        final FieldEndBytecodeEvent aFieldEndBytecodeEvent)
    {
        if (hasPropertyAnnotation) {
            extendGetPropertyField();
            extendSetPropertyField();
            hasPropertyAnnotation = false;
        }

        fieldName = null;
    }

    private void handleMethodStartBytecodeEvent(final BytecodeContext aBytecodeContext,
        final MethodStartBytecodeEvent aMethodStartBytecodeEvent)
    {
        methodName = aMethodStartBytecodeEvent.getName();
        methodDescriptor = aMethodStartBytecodeEvent.getDescriptor();
        propertyName = methodName;
    }

    private void handleMethodEndBytecodeEvent(final BytecodeContext aBytecodeContext,
        final MethodEndBytecodeEvent aMethodEndBytecodeEvent)
    {
        if (hasPropertyAnnotation) {
            switch (propertyAccess) {
            case READ_ONLY:
                extendGetPropertyMethod();
                break;
            case WRITE_ONLY:
                extendSetPropertyMethod();
                break;
            default:
                throw new BytecodeException(String.format(
                    "@Property annotated method '%s' in class '%s' declares unsupported access modifier '%s'.",
                    methodName, className, propertyAccess));
            }
            hasPropertyAnnotation = false;
        }

        methodName = null;
    }

    private void handleClassEndBytecodeEvent(final BytecodeContext aBytecodeContext,
        final ClassEndBytecodeEvent aClassEndBytecodeEvent)
    {
        finishGetProperty();
        finishSetProperty();
        aBytecodeContext.forwardBytecodeEvents(getPropertyBytecodeEvents);
        aBytecodeContext.forwardBytecodeEvents(setPropertyBytecodeEvents);
    }

    private void prepareGetProperty() {
        getPropertyBytecodeEvents = new ArrayList<>();
        getPropertyBytecodeGenerator = Bytecode.createGenerator(getPropertyBytecodeEvents);

        getPropertyBytecodeGenerator.startMethod(Opcodes.ACC_PUBLIC, "getProperty", Object.class, String.class);
        getPropertyBytecodeGenerator.startCode();
    }

    private void extendGetPropertyField() {
        final Label nextNameLabel = new Label();

        getPropertyBytecodeGenerator.pushConstant(propertyName);
        getPropertyBytecodeGenerator.loadReference(1);
        getPropertyBytecodeGenerator.invokeVirtual(String.class, "equals", boolean.class, Object.class);
        getPropertyBytecodeGenerator.jumpEquals(nextNameLabel);

        getPropertyBytecodeGenerator.loadReference(0);
        getPropertyBytecodeGenerator.loadField(className, fieldName, fieldDescriptor);
        getPropertyBytecodeGenerator.returnReference(Type.getType(fieldDescriptor));

        getPropertyBytecodeGenerator.markLabel(nextNameLabel);
        getPropertyBytecodeGenerator.markFrameSame();
    }

    private void extendGetPropertyMethod() {
        final Label nextNameLabel = new Label();

        getPropertyBytecodeGenerator.pushConstant(propertyName);
        getPropertyBytecodeGenerator.loadReference(1);
        getPropertyBytecodeGenerator.invokeVirtual(String.class, "equals", boolean.class, Object.class);
        getPropertyBytecodeGenerator.jumpEquals(nextNameLabel);

        getPropertyBytecodeGenerator.loadReference(0);
        getPropertyBytecodeGenerator.invokeVirtual(className, methodName, methodDescriptor);
        getPropertyBytecodeGenerator.returnReference(Type.getReturnType(methodDescriptor));

        getPropertyBytecodeGenerator.markLabel(nextNameLabel);
        getPropertyBytecodeGenerator.markFrameSame();
    }

    private void finishGetProperty() {
        getPropertyBytecodeGenerator.newReference(CardException.class);
        getPropertyBytecodeGenerator.duplicateReference();

        getPropertyBytecodeGenerator.newReference(StringBuilder.class);
        getPropertyBytecodeGenerator.duplicateReference();
        getPropertyBytecodeGenerator.pushConstant("Unsupported property read-request to property '");
        getPropertyBytecodeGenerator.invokeConstructor(StringBuilder.class, String.class);
        getPropertyBytecodeGenerator.loadReference(1);
        getPropertyBytecodeGenerator.invokeVirtual(StringBuilder.class, "append", StringBuilder.class, String.class);
        getPropertyBytecodeGenerator.pushConstant("' in  class '" + className + "'");
        getPropertyBytecodeGenerator.invokeVirtual(StringBuilder.class, "append", StringBuilder.class, String.class);
        getPropertyBytecodeGenerator.invokeVirtual(StringBuilder.class, "toString", String.class);

        getPropertyBytecodeGenerator.invokeConstructor(CardException.class, String.class);
        getPropertyBytecodeGenerator.throwReference();

        getPropertyBytecodeGenerator.endCode(2, 5);
        setPropertyBytecodeGenerator.endMethod();
    }

    private void prepareSetProperty() {
        setPropertyBytecodeEvents = new ArrayList<>();
        setPropertyBytecodeGenerator = Bytecode.createGenerator(setPropertyBytecodeEvents);

        setPropertyBytecodeGenerator.startMethod(Opcodes.ACC_PUBLIC, "setProperty", void.class, String.class,
            Object.class);
        setPropertyBytecodeGenerator.startCode();
    }

    private void extendSetPropertyField() {
        final Label nextPropertyNameLabel = new Label();
        final Label storeFieldLabel = new Label();
        final Type fieldType = Type.getType(fieldDescriptor);

        extendSetPropertyTestPropertyName(nextPropertyNameLabel);
        // TODO Implement type conversion via Weave Conversions
        extendSetPropertyTestPropertyType(fieldType, storeFieldLabel);
        extendSetPropertyThrowCardException();

        setPropertyBytecodeGenerator.markLabel(storeFieldLabel);
        setPropertyBytecodeGenerator.markFrameSame();

        // TODO Implement type conversion via Weave Conversions
        setPropertyBytecodeGenerator.loadReference(0);
        setPropertyBytecodeGenerator.loadReference(2);
        setPropertyBytecodeGenerator.castChecked(fieldType);
        setPropertyBytecodeGenerator.storeField(className, fieldName, fieldDescriptor);
        setPropertyBytecodeGenerator.returnVoid();

        setPropertyBytecodeGenerator.markLabel(nextPropertyNameLabel);
        setPropertyBytecodeGenerator.markFrameSame();
    }

    private void extendSetPropertyMethod() {
        final Label nextPropertyNameLabel = new Label();
        final Label invokeMethodLabel = new Label();
        final Type argumentType = Type.getArgumentTypes(methodDescriptor)[0];

        extendSetPropertyTestPropertyName(nextPropertyNameLabel);
        // TODO Implement type conversion via Weave Conversions
        extendSetPropertyTestPropertyType(argumentType, invokeMethodLabel);
        extendSetPropertyThrowCardException();

        setPropertyBytecodeGenerator.markLabel(invokeMethodLabel);
        setPropertyBytecodeGenerator.markFrameSame();

        // TODO Implement type conversion via Weave Conversions
        // Dazu privates Feld einführen und in prepareSet/prepareGet dieses Feld mit ConverterService initialisieren
        // (aus Context auslesen) (Introduktion von zusätzlichen @Inject annotierten Feldern geht wohl nicht, da
        // vermutlich die Injection-Transformation dieser Transformation hier zuvor kommen würde [prüfen]).
        setPropertyBytecodeGenerator.loadReference(0);
        setPropertyBytecodeGenerator.loadReference(2);
        setPropertyBytecodeGenerator.castChecked(argumentType);
        setPropertyBytecodeGenerator.invokeVirtual(className, methodName, methodDescriptor);
        setPropertyBytecodeGenerator.returnVoid();

        setPropertyBytecodeGenerator.markLabel(nextPropertyNameLabel);
        setPropertyBytecodeGenerator.markFrameSame();
    }

    private void extendSetPropertyTestPropertyName(final Label aNextPropertyNameLabel) {
        // <<propertyName>>.equals(<<1>>)
        // IFEQ <<nextNameLabel>>
        setPropertyBytecodeGenerator.pushConstant(propertyName);
        setPropertyBytecodeGenerator.loadReference(1);
        setPropertyBytecodeGenerator.invokeVirtual(String.class, "equals", boolean.class, Object.class);
        setPropertyBytecodeGenerator.jumpEquals(aNextPropertyNameLabel);
    }

    private void extendSetPropertyTestPropertyType(final Type aPropertyType, final Label aPropertyTypeOkLabel) {
        // <<2>> instanceof <<aPropertyType>>
        // IFNE <<aPropertyTypeOkLabel>>
        setPropertyBytecodeGenerator.loadReference(2);
        setPropertyBytecodeGenerator.instanceOf(aPropertyType);
        setPropertyBytecodeGenerator.jumpNotEquals(aPropertyTypeOkLabel);
    }

    private void extendSetPropertyThrowCardException() {
        // throw new CardException("Cannot store value '<<2>>' of type '<<2>>.getClass()' into property
        // '<<propertyName>>' in class '<<className>>')
        setPropertyBytecodeGenerator.newReference(CardException.class);
        setPropertyBytecodeGenerator.duplicateReference();

        setPropertyBytecodeGenerator.newReference(StringBuilder.class);
        setPropertyBytecodeGenerator.duplicateReference();
        setPropertyBytecodeGenerator.pushConstant("Cannot store value '");
        setPropertyBytecodeGenerator.invokeConstructor(StringBuilder.class, String.class);
        setPropertyBytecodeGenerator.loadReference(2);
        setPropertyBytecodeGenerator.invokeVirtual(StringBuilder.class, "append", StringBuilder.class, Object.class);
        setPropertyBytecodeGenerator.pushConstant("' of type '");
        setPropertyBytecodeGenerator.invokeVirtual(StringBuilder.class, "append", StringBuilder.class, String.class);
        setPropertyBytecodeGenerator.loadReference(2);
        setPropertyBytecodeGenerator.invokeVirtual(Object.class, "getClass", Class.class);
        setPropertyBytecodeGenerator.invokeVirtual(StringBuilder.class, "append", StringBuilder.class, Object.class);
        setPropertyBytecodeGenerator
            .pushConstant("' into property '" + propertyName + "' in class '" + className + "'");
        setPropertyBytecodeGenerator.invokeVirtual(StringBuilder.class, "append", StringBuilder.class, String.class);
        setPropertyBytecodeGenerator.invokeVirtual(StringBuilder.class, "toString", String.class);

        setPropertyBytecodeGenerator.invokeConstructor(CardException.class, String.class);
        setPropertyBytecodeGenerator.throwReference();
    }

    private void finishSetProperty() {
        setPropertyBytecodeGenerator.newReference(CardException.class);
        setPropertyBytecodeGenerator.duplicateReference();

        setPropertyBytecodeGenerator.newReference(StringBuilder.class);
        setPropertyBytecodeGenerator.duplicateReference();
        setPropertyBytecodeGenerator.pushConstant("Unsupported property write-request to property '");
        setPropertyBytecodeGenerator.invokeConstructor(StringBuilder.class, String.class);
        setPropertyBytecodeGenerator.loadReference(1);
        setPropertyBytecodeGenerator.invokeVirtual(StringBuilder.class, "append", StringBuilder.class, String.class);
        setPropertyBytecodeGenerator.pushConstant("' in  class '" + className + "'");
        setPropertyBytecodeGenerator.invokeVirtual(StringBuilder.class, "append", StringBuilder.class, String.class);
        setPropertyBytecodeGenerator.invokeVirtual(StringBuilder.class, "toString", String.class);

        setPropertyBytecodeGenerator.invokeConstructor(CardException.class, String.class);
        setPropertyBytecodeGenerator.throwReference();

        getPropertyBytecodeGenerator.endCode(5, 3);
        getPropertyBytecodeGenerator.endMethod();
    }
}
